package org.unidata.mdm.rest.v1.data.service.atomic;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.data.DataRecord;
import org.unidata.mdm.core.type.data.OperationType;
import org.unidata.mdm.core.type.model.RelationElement;
import org.unidata.mdm.data.context.DeleteRelationRequestContext;
import org.unidata.mdm.data.context.DeleteRelationsRequestContext;
import org.unidata.mdm.data.context.UpsertRelationRequestContext;
import org.unidata.mdm.data.context.UpsertRelationsRequestContext;
import org.unidata.mdm.data.context.UpsertRequestContext;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.rest.system.service.TypedRestInputRenderer;
import org.unidata.mdm.rest.v1.data.converter.DataRecordEtalonConverter;
import org.unidata.mdm.rest.v1.data.converter.RelationToEtalonConverter;
import org.unidata.mdm.rest.v1.data.module.DataRestModule;
import org.unidata.mdm.rest.v1.data.ro.atomic.AtomicDataUpsertRequestRO;
import org.unidata.mdm.rest.v1.data.ro.keys.LsnRO;
import org.unidata.mdm.rest.v1.data.ro.keys.RecordExternalIdRO;
import org.unidata.mdm.rest.v1.data.ro.records.DataRecordRO;
import org.unidata.mdm.rest.v1.data.ro.records.EtalonRelationToRO;
import org.unidata.mdm.rest.v1.data.ro.records.UpsertRequestRO;
import org.unidata.mdm.rest.v1.data.ro.relations.RecordRelationRO;
import org.unidata.mdm.rest.v1.data.ro.relations.RelationContainsWrapperRO;
import org.unidata.mdm.rest.v1.data.ro.relations.RelationDeleteWrapperRO;
import org.unidata.mdm.rest.v1.data.ro.relations.RelationManyToManyWrapperRO;
import org.unidata.mdm.rest.v1.data.ro.relations.RelationReferencesWrapperRO;
import org.unidata.mdm.system.context.InputCollector;
import org.unidata.mdm.system.type.rendering.FieldDef;
import org.unidata.mdm.system.type.rendering.FragmentDef;
import org.unidata.mdm.system.type.rendering.MapInputSource;
import org.unidata.mdm.system.util.ConvertUtils;

/**
 * Atomic upsert request renderer
 *
 * @author Alexandr Serov
 * @since 09.11.2020
 **/
public class AtomicDataUpsertRequestRenderer extends TypedRestInputRenderer<AtomicDataUpsertRequestRO> {

    private static final RecordExternalIdRO NULL_EXT_ID = new RecordExternalIdRO();
    private static final LsnRO NULL_LSN = new LsnRO(null, null);

    private final MetaModelService metaModelService;

    public AtomicDataUpsertRequestRenderer(MetaModelService metaModelService) {
        super(AtomicDataUpsertRequestRO.class, Collections.singletonList(
            FieldDef.fieldDef(DataRestModule.MODULE_ID, UpsertRequestRO.class))
        );
        this.metaModelService = metaModelService;
    }

    private OperationType resolveOperationType(String operationName) {
        return StringUtils.isNotBlank(operationName) ? OperationType.valueOf(operationName) : null;
    }

    @Override
    protected void renderFields(FragmentDef fragmentDef, InputCollector collector, AtomicDataUpsertRequestRO source, MapInputSource fieldValues) {
        UpsertRequestRO upsertRequest = fieldValues.get(DataRestModule.MODULE_ID, UpsertRequestRO.class);
        if (upsertRequest != null && (collector instanceof UpsertRequestContext.UpsertRequestContextBuilder)) {
            renderUpsertRequest(upsertRequest, (UpsertRequestContext.UpsertRequestContextBuilder) collector);
        }
    }

    public UpsertRequestContext createUpsertRequestContext(UpsertRequestRO upsertRequest) {
        UpsertRequestContext result = renderUpsertRequest(upsertRequest, UpsertRequestContext.builder()).build();
        OperationType operationType = resolveOperationType(upsertRequest.getOperationType());
        if (operationType == OperationType.COPY) {
            result.operationType(operationType);
        }
        return result;
    }

    private UpsertRequestContext.UpsertRequestContextBuilder renderUpsertRequest(UpsertRequestRO upsertRequest, UpsertRequestContext.UpsertRequestContextBuilder ctx) {
        DataRecordRO record = upsertRequest.getDataRecord();
        RecordExternalIdRO extId = ObjectUtils.defaultIfNull(record.getExternalId(), NULL_EXT_ID);
        LsnRO lsn = ObjectUtils.defaultIfNull(record.getLsn(), NULL_LSN);
        String entityName = record.getEntityName();
        RelationUpsertCollector relations = new RelationUpsertCollector(entityName, upsertRequest.getDraftId());
        Optional.ofNullable(upsertRequest.getRelationReferences()).ifPresent(relations::append);
        Optional.ofNullable(upsertRequest.getRelationContains()).ifPresent(relations::append);
        Optional.ofNullable(upsertRequest.getRelationManyToMany()).ifPresent(relations::append);
        OperationType operationType = resolveOperationType(upsertRequest.getOperationType());
        ctx.record(DataRecordEtalonConverter.from(record))
            .draftId(upsertRequest.getDraftId())
            .etalonKey(record.getEtalonId())
            .externalId(extId.getExternalId())
            .sourceSystem(extId.getSourceSystem())
            .lsn(lsn.getLsn())
            .shard(lsn.getShard())
            .entityName(record.getEntityName())
            .validFrom(ConvertUtils.localDateTime2Date(record.getValidFrom()))
            .validTo(ConvertUtils.localDateTime2Date(record.getValidTo()))
            .suppressWorkflow(operationType == OperationType.COPY);
        if (!relations.upsert.isEmpty()) {
            ctx.fragment(UpsertRelationsRequestContext.builder().relationsFrom(relations.upsert.stream()
                .collect(Collectors.groupingBy(UpsertRelationRequestContext::getRelationName))).build());
        }
        if (!relations.delete.isEmpty()) {
            ctx.fragment(DeleteRelationsRequestContext.builder().relationsFrom(relations.delete.stream()
                .collect(Collectors.groupingBy(DeleteRelationRequestContext::getRelationName))).build());
        }
        return ctx;
    }


    private class RelationUpsertCollector {

        private final String entityName;
        private final Long parentDraftId;
        private final List<UpsertRelationRequestContext> upsert = new ArrayList<>();
        private final List<DeleteRelationRequestContext> delete = new ArrayList<>();

        private RelationUpsertCollector(String entityName, Long parentDraftId) {
            this.entityName = entityName;
            this.parentDraftId = parentDraftId;
        }

        private void append(RelationReferencesWrapperRO contains) {
            List<EtalonRelationToRO> updates = ObjectUtils.defaultIfNull(contains.getToUpdate(), Collections.emptyList());
            List<RelationDeleteWrapperRO> deletes = ObjectUtils.defaultIfNull(contains.getToDelete(), Collections.emptyList());
            updates.forEach(this::append);
            deletes.forEach(this::append);
        }

        private void append(RelationContainsWrapperRO reference) {
            List<RecordRelationRO> updates = ObjectUtils.defaultIfNull(reference.getToUpdate(), Collections.emptyList());
            List<RelationDeleteWrapperRO> deletes = ObjectUtils.defaultIfNull(reference.getToDelete(), Collections.emptyList());
            updates.forEach(this::append);
            deletes.forEach(this::append);
        }

        private void append(RelationManyToManyWrapperRO manyToMany) {
            List<EtalonRelationToRO> updates = ObjectUtils.defaultIfNull(manyToMany.getToUpdate(), Collections.emptyList());
            List<RelationDeleteWrapperRO> deletes = ObjectUtils.defaultIfNull(manyToMany.getToDelete(), Collections.emptyList());
            updates.forEach(this::append);
            deletes.forEach(this::append);
        }

        private void append(RecordRelationRO ro) {
            DataRecordRO dro = ro.getRecord();
            DataRecord converted;
            if (dro != null && (converted = DataRecordEtalonConverter.from(dro)) != null) {

                String toEtalonId = dro.getEtalonId();
                RecordExternalIdRO extId = dro.getExternalId();

                RelationElement el = Objects.nonNull(ro.getRelationName())
                        ? metaModelService.instance(Descriptors.DATA).getRelation(ro.getRelationName())
                        : null;

                String toEntityName = el != null ? el.getRight().getName() : null;

                upsert.add(UpsertRelationRequestContext.builder()
                    .relationEtalonKey(ro.getRelationEtalonKey())
                    .etalonKey(toEtalonId)
                    .sourceSystem(Objects.nonNull(extId) ? extId.getSourceSystem() : null)
                    .externalId(Objects.nonNull(extId) ? extId.getExternalId() : null)
                    .entityName(toEntityName)
                    .record(converted)
                    .parentDraftId(parentDraftId)
                    .relationName(ro.getRelationName())
                    .draftId(ro.getDraftId())
                    .validFrom(ConvertUtils.localDateTime2Date(dro.getValidFrom()))
                    .validTo(ConvertUtils.localDateTime2Date(dro.getValidTo()))
                    .build()
                );
            }
        }

        private void append(EtalonRelationToRO ro) {
            DataRecord converted = RelationToEtalonConverter.from(ro);
            if (converted != null) {
                upsert.add(UpsertRelationRequestContext.builder()
                    .etalonKey(ro.getEtalonIdTo())
                    .record(converted)
                    .parentDraftId(parentDraftId)
                    .draftId(ro.getDraftId())
                    .relationName(ro.getRelName())
                    .validFrom(ConvertUtils.localDateTime2Date(ro.getValidFrom()))
                    .validTo(ConvertUtils.localDateTime2Date(ro.getValidTo()))
                    .sourceSystem(metaModelService.instance(Descriptors.SOURCE_SYSTEMS).getAdminElement().getName())
                    .build());
            }
        }

        private void append(RelationDeleteWrapperRO dro) {
            delete.add(DeleteRelationRequestContext.builder()
                .relationName(dro.getRelName())
                .entityName(entityName)
                .relationEtalonKey(dro.getEtalonRelationId())
                .inactivateEtalon(dro.isInactivateEtalon())
                .relationOriginKey(dro.getOriginRelationId())
                .inactivateOrigin(dro.isInactivateOrigin())
                .inactivatePeriod(dro.isInactivatePeriod())
                .wipe(dro.isWipe())
                .parentDraftId(parentDraftId)
                .draftId(dro.getDraftId())
                .validFrom(ConvertUtils.localDateTime2Date(dro.getValidFrom()))
                .validTo(ConvertUtils.localDateTime2Date(dro.getValidTo()))
                .build()
            );
        }

    }
}
